/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "napi/native_api.h"
#include <cstdio>
#include <js_native_api.h>
#include <js_native_api_types.h>
#include <cstdlib>
#include "zstd.h"
#include <hilog/log.h>

struct ExtractCompressCallBack {
    napi_async_work work;
    napi_ref callbackRef = nullptr; // 用于callback模式
    napi_deferred deferred;         // 用于promise模式
    char *srcPath;
    char *archive;
    int level;
    int nbThreads;
    int value;
};

struct ExtractDecompressCallBack {
    napi_async_work work;
    napi_ref callbackRef = nullptr; // 用于callback模式
    napi_deferred deferred;         // 用于promise模式
    char *archivePath;
    char *destPath;
    int value;
};

static size_t compressFile(char *srcPath, char *archive, int level, int nbThreads) {
    size_t buffInSize = 0;
    size_t buffOutSize = 0;
    char *srcBuff = nullptr;
    char *desBuff = nullptr;
    ZSTD_CCtx *createDctx = nullptr;
    size_t value = -1; // 返回值

    /* Open the input and output files. */
    FILE *fileIn = fopen(srcPath, "rb");
    FILE *fileOut = fopen(archive, "wb");
    if (fileIn == nullptr || fileOut == nullptr) {
        goto EXIT;
    }
    /* Create the input and output buffers.
     * They may be any size, but we recommend using these functions to size them.
     * Performance will only suffer significantly for very tiny buffers.
     */
    buffInSize = ZSTD_CStreamInSize();
    buffOutSize = ZSTD_CStreamOutSize();
    if (buffInSize <= 0 || buffOutSize <= 0) {
        goto EXIT;
    }

    srcBuff = (char *)malloc(buffInSize * sizeof(char));
    desBuff = (char *)malloc(buffOutSize * sizeof(char));
    if (srcBuff == nullptr || desBuff == nullptr) {
        goto EXIT;
    }

    /* Create the context. */
    createDctx = ZSTD_createCCtx();
    if (createDctx == nullptr) {
        goto EXIT;
    }

    /* Set any parameters you want.
     * Here we set the compression level, and enable the checksum.
     */
    ZSTD_CCtx_setParameter(createDctx, ZSTD_c_compressionLevel, level);
    ZSTD_CCtx_setParameter(createDctx, ZSTD_c_checksumFlag, 1);
    ZSTD_CCtx_setParameter(createDctx, ZSTD_c_nbWorkers, nbThreads);

    /* This loop read from the input file, compresses that entire chunk,
     * and writes all output produced to the output file.
     */
    for (;;) {
        size_t readLength = fread(srcBuff, 1, buffInSize, fileIn);
        if (readLength <= 0) {
            goto EXIT;
        }
        /* Select the flush mode.
         * If the read may not be finished (read == toRead) we use
         * ZSTD_e_continue. If this is the last chunk, we use ZSTD_e_end.
         * Zstd optimizes the case where the first flush mode is ZSTD_e_end,
         * since it knows it is compressing the entire source in one pass.
         */
        int lastChunk = (readLength < buffInSize);
        ZSTD_EndDirective mode = lastChunk ? ZSTD_e_end : ZSTD_e_continue;
        /* Set the input buffer to what we just read.
         * We compress until the input buffer is empty, each time flushing the
         * output.
         */
        ZSTD_inBuffer input = {srcBuff, readLength, 0};
        int finished;
        do {
            /* Compress into the output buffer and write all of the output to
             * the file so we can reuse the buffer next iteration.
             */
            ZSTD_outBuffer output = {desBuff, buffOutSize, 0};
            size_t remaining = ZSTD_compressStream2(createDctx, &output, &input, mode);
            value = remaining;
            if (ZSTD_isError(remaining)) {
                value = -1;
                goto EXIT;
            }
            if (fwrite(desBuff, 1, output.pos, fileOut) <= 0) {
                value = -1;
                goto EXIT;
            }
            /* If we're on the last chunk we're finished when zstd returns 0,
             * which means its consumed all the input AND finished the frame.
             * Otherwise, we're finished when we've consumed all the input.
             */
            finished = lastChunk ? (remaining == 0) : (input.pos == input.size);
        } while (!finished);

        if (lastChunk) {
            break;
        }
    }

EXIT:
    if (createDctx != nullptr) {
        ZSTD_freeCCtx(createDctx);
    }
    if (desBuff != nullptr) {
        free(desBuff);
    }
    if (srcBuff != nullptr) {
        free(srcBuff);
    }
    if (fileOut != nullptr) {
        fclose(fileOut);
    }
    if (fileIn != nullptr) {
        fclose(fileIn);
    }
    if (archive != nullptr) {
        free(archive);
    }
    if (srcPath != nullptr) {
        free(srcPath);
    }
    return value;
}

static size_t decompressFile(char *archivePath, char *destPath) {
    size_t buffInSize = 0;
    size_t buffOutSize = 0;
    char *srcBuff = nullptr;
    char *desBuff = nullptr;
    ZSTD_DCtx *createDctx = nullptr;
    size_t readLength = 0;
    size_t lastRet = 0;
    int isEmpty = 1;
    size_t value = -1; // 返回值

    /* Open the input and output files. */
    FILE *fileIn = fopen(archivePath, "rb");
    FILE *fileOut = fopen(destPath, "wb");
    if (fileIn == nullptr || fileOut == nullptr) {
        goto EXIT;
    }

    buffInSize = ZSTD_DStreamInSize();
    buffOutSize = ZSTD_DStreamOutSize();
    if (buffInSize <= 0 || buffOutSize <= 0) {
        goto EXIT;
    }

    srcBuff = (char *)malloc(buffInSize * sizeof(char));
    desBuff = (char *)malloc(buffOutSize * sizeof(char));
    if (srcBuff == nullptr || desBuff == nullptr) {
        goto EXIT;
    }

    createDctx = ZSTD_createDCtx();
    if (createDctx == nullptr) {
        goto EXIT;
    }
    /* This loop assumes that the input file is one or more concatenated zstd
     * streams. This example won't work if there is trailing non-zstd data at
     * the end, but streaming decompression in general handles this case.
     * ZSTD_decompressStream() returns 0 exactly when the frame is completed,
     * and doesn't consume input after the frame.
     */
    while ((readLength = fread(srcBuff, 1, buffInSize, fileIn))) {
        isEmpty = 0;
        ZSTD_inBuffer input = {srcBuff, readLength, 0};
        /* Given a valid frame, zstd won't consume the last byte of the frame
         * until it has flushed all of the decompressed data of the frame.
         * Therefore, instead of checking if the return code is 0, we can
         * decompress just check if input.pos < input.size.
         */
        while (input.pos < input.size) {
            ZSTD_outBuffer output = {desBuff, buffOutSize, 0};
            /* The return code is zero if the frame is complete, but there may
             * be multiple frames concatenated together. Zstd will automatically
             * reset the context when a frame is complete. Still, calling
             * ZSTD_DCtx_reset() can be useful to reset the context to a clean
             * state, for instance if the last decompression call returned an
             * error.
             */
            size_t decompressSize = ZSTD_decompressStream(createDctx, &output, &input);
            value = decompressSize;
            if (ZSTD_isError(decompressSize)) {
                value = -1;
                goto EXIT;
            }
            if (fwrite(desBuff, 1, output.pos, fileOut) <= 0) {
                value = -1;
                goto EXIT;
            }
            lastRet = decompressSize;
        }
    }

    if (isEmpty) {
        value = -1;
        goto EXIT;
    }

    if (lastRet != 0) {
        /* The last return value from ZSTD_decompressStream did not end on a
         * frame, but we reached the end of the file! We assume this is an
         * error, and the input was truncated.
         */
        value = -1;
        goto EXIT;
    }
EXIT:
    if (createDctx != nullptr) {
        ZSTD_freeDCtx(createDctx);
    }
    if (desBuff != nullptr) {
        free(desBuff);
    }
    if (srcBuff != nullptr) {
        free(srcBuff);
    }
    if (fileOut != nullptr) {
        fclose(fileOut);
    }
    if (fileIn != nullptr) {
        fclose(fileIn);
    }
    if (destPath != nullptr) {
        free(destPath);
    }
    if (archivePath != nullptr) {
        free(archivePath);
    }
    return value;
}

static void ZstdCompressExecute(napi_env env, void *datas) {
    // napi_async_execute_callback方法，该方法会在新起的线程中运行，一般在此函数中调用C++接口进行异步处理操作。
    ExtractCompressCallBack *callback = static_cast<ExtractCompressCallBack *>(datas);
    size_t value = compressFile(callback->srcPath, callback->archive, callback->level, callback->nbThreads);
    if (value != -1) {
        callback->value = 1;
    } else {
        callback->value = -1;
    }
}

static void ZstdDecompressExecute(napi_env env, void *datas) {
    // napi_async_execute_callback方法，该方法会在新起的线程中运行，一般在此函数中调用C++接口进行异步处理操作。
    ExtractDecompressCallBack *callback = static_cast<ExtractDecompressCallBack *>(datas);
    size_t value = decompressFile(callback->archivePath, callback->destPath);
    if (value != -1) {
        callback->value = 1;
    } else {
        callback->value = -1;
    }
}

static napi_value ZstdCompress(napi_env env, napi_callback_info info) {
    size_t requireArgc = 4;
    size_t argc = 4;
    napi_value args[4] = {nullptr};

    OH_LOG_Print(LOG_APP, LOG_INFO, 0, "zstd", "ZstdCompress");
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    if (argc < requireArgc) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Wrong arguments. 4 arguments are expected.");
        napi_throw_error(env, nullptr, "Wrong arguments. 4 arguments are expected.");
        return nullptr;
    }

    // 获取参数
    size_t srcSize = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &srcSize);
    if (srcSize <= 0) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to get the number of bytes for 'src'");
        napi_throw_error(env, nullptr, "Failed to get the number of bytes for 'src'");
        return nullptr;
    }
    srcSize = srcSize + 1;
    char *srcPath = (char *)malloc(srcSize * sizeof(char));
    if (srcPath == nullptr) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to allocate memory for 'src'");
        napi_throw_error(env, nullptr, "Failed to allocate memory for 'src'");
        return nullptr;
    }
    size_t srcPathSize = 0;
    napi_get_value_string_utf8(env, args[0], srcPath, srcSize, &srcPathSize);

    size_t archiveSize = 0;
    napi_get_value_string_utf8(env, args[1], nullptr, 0, &archiveSize);
    if (archiveSize <= 0) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to get the number of bytes for 'archive'");
        napi_throw_error(env, nullptr, "Failed to get the number of bytes for 'archive'");
        return nullptr;
    }
    archiveSize = archiveSize + 1;
    char *archive = (char *)malloc(archiveSize * sizeof(char));
    if (archive == nullptr) {
        free(srcPath);
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to allocate memory for 'archive'");
        napi_throw_error(env, nullptr, "Failed to allocate memory for 'archive'");
        return nullptr;
    }
    size_t archivePathSize = 0;
    napi_get_value_string_utf8(env, args[1], archive, archiveSize, &archivePathSize);

    int level;
    int levelParameter = 2;
    napi_get_value_int32(env, args[levelParameter], &level);

    int nbThreads;
    int nbThreadsParameter = 3;
    napi_get_value_int32(env, args[nbThreadsParameter], &nbThreads);

    ExtractCompressCallBack *callback = new ExtractCompressCallBack();
    // 解析入参
    callback->srcPath = srcPath;
    callback->archive = archive;
    callback->level = level;
    callback->nbThreads = nbThreads;

    // 说明是promise模式，
    napi_value promise = nullptr;
    napi_create_promise(env, &callback->deferred, &promise);
    napi_value resource = nullptr;
    napi_create_string_utf8(env, "ZstdCompress", NAPI_AUTO_LENGTH, &resource);
    // 创建异步工作
    napi_create_async_work(
        env, nullptr, resource,
        ZstdCompressExecute,
        [](napi_env env, napi_status status, void *data) {
            ExtractCompressCallBack *callback = static_cast<ExtractCompressCallBack *>(data);
            napi_value result[2] = {0};
            napi_create_double(env, callback->value, &result[1]);
            napi_resolve_deferred(env, callback->deferred, result[1]);
            napi_delete_async_work(env, callback->work); // 异步任务完成之后删除任务
            delete callback;
        },
        (void *)callback, &callback->work);
    napi_queue_async_work(env, callback->work); // 异步任务入队列，排队执行
    return promise;
}

static napi_value ZstdDecompress(napi_env env, napi_callback_info info) {
    size_t requireArgc = 2;
    size_t argc = 2;
    napi_value args[2] = {nullptr};
    OH_LOG_Print(LOG_APP, LOG_INFO, 0, "zstd", "ZstdDecompress");
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    if (argc < requireArgc) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Wrong arguments. 2 arguments are expected.");
        napi_throw_error(env, nullptr, "Wrong arguments. 2 arguments are expected.");
        return nullptr;
    }
    // 获取参数
    size_t archiveSize = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &archiveSize);
    if (archiveSize <= 0) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to get the number of bytes for 'archive'.");
        napi_throw_error(env, nullptr, "Failed to get the number of bytes for 'archive'");
        return nullptr;
    }
    archiveSize = archiveSize + 1;
    char *archivePath = (char *)malloc(archiveSize * sizeof(char));
    if (archivePath == nullptr) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to get the number of bytes for 'archive'.");
        napi_throw_error(env, nullptr, "Failed to get the number of bytes for 'archive'");
        return nullptr;
    }
    size_t archivePathSize = 0;
    napi_get_value_string_utf8(env, args[0], archivePath, archiveSize, &archivePathSize);

    size_t destSize = 0;
    napi_get_value_string_utf8(env, args[1], nullptr, 0, &destSize);
    if (destSize <= 0) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to get the number of bytes for 'archive'.");
        napi_throw_error(env, nullptr, "Failed to get the number of bytes for 'archive'");
        return nullptr;
    }
    destSize = destSize + 1;
    char *destPath = (char *)malloc(destSize * sizeof(char));
    if (destPath == nullptr) {
        free(archivePath);
        OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "Failed to allocate memory for 'dest'.");
        napi_throw_error(env, nullptr, "Failed to allocate memory for 'dest'");
        return nullptr;
    }
    size_t destPathSize = 0;
    napi_get_value_string_utf8(env, args[1], destPath, destSize, &destPathSize);

    ExtractDecompressCallBack *callback = new ExtractDecompressCallBack();
    // 解析入参
    callback->archivePath = archivePath;
    callback->destPath = destPath;
    // 说明是promise模式，
    napi_value promise = nullptr;
    napi_create_promise(env, &callback->deferred, &promise);
    napi_value resource = nullptr;
    napi_create_string_utf8(env, "ZstdDecompress", NAPI_AUTO_LENGTH, &resource);
    // 创建异步工作
    napi_create_async_work(
        env, nullptr, resource,
        ZstdDecompressExecute,
        [](napi_env env, napi_status status, void *data) {
            ExtractDecompressCallBack *callback = static_cast<ExtractDecompressCallBack *>(data);
            napi_value result[2] = {0};
            napi_create_double(env, callback->value, &result[1]);
            napi_resolve_deferred(env, callback->deferred, result[1]);
            napi_delete_async_work(env, callback->work); // 异步任务完成之后删除任务
            delete callback;
        },
        (void *)callback, &callback->work);
    napi_queue_async_work(env, callback->work); // 异步任务入队列，排队执行
    return promise;
}

#define CCNAPI_MAJOR_VERSION 2
#define CCNAPI_MINOR_VERSION 0
#define CCNAPI_PATCH_LEVEL 5

#define CCNAPI_SX(x) #x
#define CCNAPI_S(x) CCNAPI_SX(x)

#define CCNAPI_VERSION_STRING                                        \
    CCNAPI_S(CCNAPI_MAJOR_VERSION)                                         \
    "." CCNAPI_S(CCNAPI_MINOR_VERSION) \
    "." CCNAPI_S(CCNAPI_PATCH_LEVEL)

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "zstd", "VERSION : %{public}s", CCNAPI_VERSION_STRING);
    napi_property_descriptor desc[] = {
        {"zstdCompress", nullptr, ZstdCompress, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"zstdDecompress", nullptr, ZstdDecompress, nullptr, nullptr, nullptr, napi_default, nullptr}};
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "zstdnapi",
    .nm_priv = ((void *)0),
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
    napi_module_register(&demoModule);
}
